---
id: serialportclient
title: 串口客户端
---

import Tag from "@site/src/components/Tag.js";

### 定义

命名空间：TouchSocket.SerialPorts <br/>
程序集：[TouchSocket.SerialPorts.dll](https://www.nuget.org/packages/TouchSocket.SerialPorts)


## 一、说明

`SerialPortClient`是串口系客户端，他直接参与串口的连接、发送、接收、处理、断开等。

## 二、特点

- 简单易用。
- 内存池支持
- 高性能
- 适配器预处理，一键式解决**分包**、**粘包**、对象解析(modbus)等。
- 超简单的同步发送、异步发送、接收等操作。
- 基于委托、插件驱动，让每一步都能执行AOP。

## 三、产品应用场景

- 所有串口基础使用场景：可跨平台、跨语言使用。
- 自定义协议解析场景：可解析任意数据格式的串口数据报文。

## 四、可配置项

<details>
<summary>可配置项</summary>
<div>

#### SetMaxPackageSize

数据包最大值（单位：byte），默认1024×1024×10。该值会在适当时间，直接作用DataHandlingAdapter.MaxPackageSize。 

#### SetSerialPortOption

配置串口相关。例如：端口、波特率、数据位、校验位、停止位等。

</div>
</details>

## 五、支持插件

|  插件方法| 功能 |
| --- | --- |
| ISerialConnectingPlugin | 在串口连接之前触发。 |
| ISerialConnectedPlugin | 同意连接，且成功启动接收后触发 |
| ISerialClosingPlugin | 当客户端主动调用`Close`时触发 |
| ISerialClosedPlugin | 当客户端断开连接后触发 |
| ISerialReceivingPlugin | 在收到原始数据时触发，所有的数据均在`ByteBlock`里面。 |
| ISerialReceivedPlugin | 在收到适配器数据时触发，根据适配器类型，数据可能在`ByteBlock`或者`IRequestInfo`里面。 |
| ISerialSendingPlugin | 当即将发送数据时，调用该方法在适配器之后，接下来即会发送数据。 |

## 六、创建SerialPortClient

#### 6.1 简单创建

简单的处理逻辑可通过**Connecting**、**Connected**、**Received**等委托直接实现。

代码如下：

```csharp showLineNumbers
var client = new SerialPortClient();
client.Connecting = (client, e) => { return EasyTask.CompletedTask; };//即将连接到端口
client.Connected = (client, e) => { return EasyTask.CompletedTask; };//成功连接到端口
client.Closing = (client, e) => { return EasyTask.CompletedTask; };//即将从端口断开连接。此处仅主动断开才有效。
client.Closed = (client, e) => { return EasyTask.CompletedTask; };//从端口断开连接，当连接不成功时不会触发。
client.Received = async (c, e) =>
{
    await Console.Out.WriteLineAsync(e.ByteBlock.Span.ToString(Encoding.UTF8));
};

await client.SetupAsync(new TouchSocketConfig()
     .SetSerialPortOption(new SerialPortOption()
     {
         BaudRate = 9600,//波特率
         DataBits = 8,//数据位
         Parity = System.IO.Ports.Parity.None,//校验位
         PortName = "COM1",//COM
         StopBits = System.IO.Ports.StopBits.One//停止位
     })
     .SetSerialDataHandlingAdapter(() => new PeriodPackageAdapter(){ CacheTimeout = TimeSpan.FromMilliseconds(100) })
     .ConfigurePlugins(a =>
     {
         a.Add<MyPlugin>();
     }));

await client.ConnectAsync();
Console.WriteLine("连接成功");
```

:::info 信息

上述示例中使用了`PeriodPackageAdapter`适配器，具体作用是确保把`100ms`内收到数据，作为一个包解析，在实际使用时，可以根据业务情况自由修改时间值。详情： [PeriodPackageAdapter](packageadapter.mdx)

:::  

#### 6.2 继承实现

一般继承实现的话，可以从`SerialPortClientBase`继承。

```csharp showLineNumbers
class MySerialClient : SerialPortClientBase
{
    protected override async Task OnSerialReceived(ReceivedDataEventArgs e)
    {
        //此处逻辑单线程处理。

        //此处处理数据，功能相当于Received委托。
        string mes = e.ByteBlock.Span.ToString(Encoding.UTF8);
        Console.WriteLine($"已接收到信息：{mes}");

        await base.OnSerialReceived(e);
    }
}
```

```csharp showLineNumbers
var client = new MySerialClient();
await client.SetupAsync(new TouchSocket.Core.TouchSocketConfig()
    .SetSerialPortOption(new SerialPortOption()
    {
        BaudRate = 9600,//波特率
        DataBits = 8,//数据位
        Parity = System.IO.Ports.Parity.None,//校验位
        PortName = "COM1",//COM
        StopBits = System.IO.Ports.StopBits.One//停止位
    }));
await client.ConnectAsync();//调用连接，当连接不成功时，会抛出异常。
```

## 七、接收数据

在串口中，接收数据的方式有很多种。多种方式可以组合使用。

### 7.1 Received委托处理

当使用`SerialPortClient`创建客户端时，内部已经定义好了一个外置委托`Received`，可以通过该委托直接接收数据。

```csharp showLineNumbers
var client = new SerialPortClient();
client.Received = (client, e) =>
{
    //收到信息
    string mes = e.ByteBlock.Span.ToString(Encoding.UTF8);
    Console.WriteLine($"接收到信息：{mes}");
    return EasyTask.CompletedTask;
};

```

### 7.2 ReadAsync异步读取

在使用串口时，可能更需要在当前代码上下文读取的情况。所以此处提供了异步读取方法。

```csharp showLineNumbers
await client.ConnectAsync();

using (var receiver = client.CreateReceiver())
{
    while (true)
    {
        using (CancellationTokenSource tokenSource=new CancellationTokenSource(TimeSpan.FromSeconds(10)))
        {
            using (var receiverResult = await receiver.ReadAsync(tokenSource.Token))
            {
                if (receiverResult.IsCompleted)
                {
                    //断开
                }

                //按照适配器类型。此处可以获取receiverResult.ByteBlock或者receiverResult.RequestInfo
                await Console.Out.WriteLineAsync(receiverResult.ByteBlock.Span.ToString(Encoding.UTF8));
            }
        }
    }
}
```

:::tip 提示

`receiver`在被`Create`之后，其优先级最高，即：`Received`委托和插件都不再被调用。

:::  

### 7.3 插件处理 <Tag>推荐</Tag>

按照`TouchSocket`的设计理念，使用插件处理数据，是一项非常简单，且高度解耦的方式。步骤如下：

（1）声明插件

插件可以先继承`PluginBase`，然后再实现需要的功能插件接口，可以按需选择泛型或者非泛型实现。

如果已经有继承类，直接实现`IPlugin`接口即可。

```csharp showLineNumbers
public class MyPlugin : PluginBase, ISerialReceivedPlugin
{
    public async Task OnSerialReceived(ISerialPortSession client, ReceivedDataEventArgs e)
    {
        //这里处理数据接收
        //根据适配器类型，e.ByteBlock与e.RequestInfo会呈现不同的值，具体看文档=》适配器部分。
        var byteBlock = e.ByteBlock;
        var requestInfo = e.RequestInfo;

        ////表示该数据已经被本插件处理，无需再投递到其他插件。

        await e.InvokeNext();
    }
}
```

（2）创建使用插件处理的客户端

```csharp  {13} showLineNumbers
var client = new SerialPortClient();
await client.SetupAsync(new TouchSocketConfig()
    .SetSerialPortOption(new SerialPortOption()
    {
        BaudRate = 9600,//波特率
        DataBits = 8,//数据位
        Parity = System.IO.Ports.Parity.None,//校验位
        PortName = "COM1",//COM
        StopBits = System.IO.Ports.StopBits.One//停止位
    })
    .ConfigurePlugins(a => 
    {
        a.Add<MyPlugin>();
    }));

await client.ConnectAsync();
```

## 八、发送数据

### 8.1 发送

`SerialPortClient`已经内置了多种发送方法，直接调用就可以发送。

但需要注意的是，通过该方法发送的数据，会经过**适配器**，如果想要直接发送，请继承以后，使用**DefaultSendAsync**。如果发送失败，则会立即抛出异常。

```csharp showLineNumbers
Task SendAsync(ReadOnlyMemory<byte> memory);
```

:::tip 提示

框架不仅内置了`SendAsync`字节的发送，也扩展了字符串等常见数据的发送。而且还包括了`TrySend`等不会抛出异常的发送方法。

::: 

### 8.2 同步发送等待

`WaitingClient`是`TouchSocket`提供的一个等待客户端发送数据的客户端。例如：串口客户端发送一个"Hello"，等待对方回信"Hi"，此时即可使用`WaitingClient`。

```csharp showLineNumbers
await client.ConnectAsync();

//调用CreateWaitingClient获取到IWaitingClient的对象。
var waitClient = client.CreateWaitingClient(new WaitingOptions()
{
    FilterFunc = response => //设置用于筛选的fun委托，当返回为true时，才会响应返回
    {
        return true;

        //if (response.Data.Length == 1)
        //{
        //    return true;
        //}
        //return false;
    }
});

//然后使用SendThenReturn。
byte[] returnData = waitClient.SendThenReturn(Encoding.UTF8.GetBytes("Hello"));
client.Logger.Info($"收到回应消息：{Encoding.UTF8.GetString(returnData)}");

//同时，如果适配器收到数据后，返回的并不是字节，而是IRequestInfo对象时，可以使用SendThenResponse.
ResponsedData responsedData = waitClient.SendThenResponse(Encoding.UTF8.GetBytes("Hello"));
IRequestInfo requestInfo = responsedData.RequestInfo;//同步收到的RequestInfo
```

:::tip 提示

在`SendThenReturn`时，通过其他参数，还可以设置`Timeout`，以及可取消的等待`Token`。

:::  

:::danger 注意事项

1. 发送完数据，在等待时，如果收到其他返回数据，则可能得到错误结果。
2. 发送采用同步锁，一个事务没结束，另一个请求也发不出去。
3. 在**Net461及以下**版本中，`SendThenReturn`与`SendThenReturnAsync`不能混合使用。即：要么全同步，要么全异步。

:::

[本文示例Demo](https://gitee.com/RRQM_Home/TouchSocket/tree/master/examples/Serial)

